﻿// Copyright (c) 2013 fancidev
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;

namespace Util.Media
{
    /// <summary>
    /// Defines an abstract class with similar interface to 
    /// System.Drawing.Image but allows custom image implementations.
    /// </summary>
    [CLSCompliant(true)]
    public abstract class MultiFrameImage : IDisposable
    {
        /// <summary>
        /// Calls the protected method <code>Dispose(true)</code>.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public virtual string Name { get; set; }

        /// <summary>
        /// Disposes resources.
        /// </summary>
        /// <param name="disposing">true if called from Dispose().</param>
        protected virtual void Dispose(bool disposing) { }

        /// <summary>
        /// Gets the width of the image in pixels.
        /// </summary>
        public abstract int Width { get; }

        /// <summary>
        /// Gets the height of the image in pixels.
        /// </summary>
        public abstract int Height { get; }

        /// <summary>
        /// Gets the width and height of the image in pixels.
        /// </summary>
        public virtual Size Size
        {
            get { return new Size(this.Width, this.Height); }
        }

        /// <summary>
        /// Gets the number of frames in the image along the Time dimension.
        /// </summary>
        public virtual int FrameCount { get { return 1; } }

        /// <summary>
        /// Gets or sets the index of the active frame along the Time
        /// dimension.
        /// </summary>
        public virtual int FrameIndex
        {
            get { return 0; }
            set
            {
                if (value != 0)
                    throw new ArgumentOutOfRangeException("value", "FrameIndex must be zero.");
            }
        }

        public virtual void AdvanceFrame(bool allowLooping)
        {
            if (this.FrameIndex == this.FrameCount - 1)
            {
                if (allowLooping)
                    this.FrameIndex = 0;
                else
                    throw new InvalidOperationException("Cannot advance from the last frame without looping.");
            }
            else
            {
                this.FrameIndex++;
            }
        }

        /// <summary>
        /// Gets the current frame of the image. The caller must not dispose
        /// the returned object; instead, it should dispose this ImageEx
        /// object when no longer used.
        /// </summary>
        public abstract Image Frame { get; }

        /// <summary>
        /// Gets the delay of the current frame, or <code>TimeSpan.Zero</code>
        /// if the underlying image doesn't support delay.
        /// </summary>
        public virtual TimeSpan FrameDelay { get { return TimeSpan.Zero; } }
    }

    /// <summary>
    /// Provides a wrapper of System.Drawing.Image to implement MultiFrameImage.
    /// </summary>
    public sealed class GdiImage : MultiFrameImage
    {
        private Image image = null;
        private int frameIndex = 0;

        /// <summary>
        /// Creates a wrapper of the given System.Drawing.Image.
        /// </summary>
        /// <param name="image">The image to wrap.</param>
        public GdiImage(Image image)
        {
            if (image == null)
                throw new ArgumentNullException("image");
            this.image = image;
        }

        /// <summary>
        /// Disposes the object.
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                image.Dispose();
            }
            base.Dispose(disposing);
        }

        /// <summary>
        /// Gets the width of the image in pixels.
        /// </summary>
        public override int Width { get { return image.Width; } }

        /// <summary>
        /// Gets the height of the image in pixels.
        /// </summary>
        public override int Height { get { return image.Height; } }

        /// <summary>
        /// Gets the width and height of the image in pixels.
        /// </summary>
        public override Size Size { get { return image.Size; } }

        /// <summary>
        /// Gets the number of frames in the image along the Time dimension.
        /// </summary>
        public override int FrameCount
        {
            get
            {
                try
                {
                    if (image.RawFormat.Guid == ImageFormat.Gif.Guid ||
                        image.RawFormat.Guid == ImageFormat.Tiff.Guid)
                        return image.GetFrameCount(FrameDimension.Time);
                    else
                        return 1;
                }
                catch (System.Runtime.InteropServices.ExternalException)
                {
                    throw;
                }
            }
        }

        /// <summary>
        /// Gets or sets the index of the active frame along the Time
        /// dimension.
        /// </summary>
        public override int FrameIndex
        {
            get { return frameIndex; }
            set
            {
                if (value != frameIndex)
                {
                    image.SelectActiveFrame(FrameDimension.Time, value);
                    frameIndex = value;
                }
            }
        }

        /// <summary>
        /// Gets the current frame of the image.
        /// </summary>
        public override Image Frame { get { return this.image; } }

        /// <summary>
        /// Gets the delay of the current frame, or <code></code>TimeSpan.Zero</code>
        /// if the underlying image doesn't support delay.
        /// </summary>
        public override TimeSpan FrameDelay
        {
            // See http://www.codeproject.com/Articles/27387/Play-GIF-using-GDI
            get
            {
                // For GIF images, an array of frame delays (expressed in 
                // 1/100 of a second) is stored in property tag 0x5100
                // (PropertyTagFrameDelay).
                try
                {
                    PropertyItem item = image.GetPropertyItem(0x5100);
                    if (item.Value.Length == 4 * FrameCount)
                    {
                        int n = BitConverter.ToInt32(item.Value, 4 * frameIndex);
                        // convert 1/100 second to 1/10000 millisecond
                        return new TimeSpan((long)n * 10000 * 10);
                    }
                }
                catch (ArgumentException) { }
                return TimeSpan.Zero;
            }
        }
    }

    /// <summary>
    /// Implements MultiFrameImage using a sequence of Image objects.
    /// </summary>
    public class GdiImageSequence : MultiFrameImage
    {
        class FrameInfo
        {
            public Image Image;
            public int Delay;
            public bool ShouldDispose;
        }
        List<FrameInfo> frames = new List<FrameInfo>();
        int currentIndex = 0;

        public void AddFrame(Image image, int delay, bool dispose)
        {
            if (frames == null)
                throw new ObjectDisposedException("FrameIndex");
            if (image == null)
                throw new ArgumentNullException("image");
            if (delay < 0)
                throw new ArgumentOutOfRangeException("delay");
            if (frames.Count > 0 && image.Size != frames[0].Image.Size)
                throw new ArgumentException("All images must have the same size.");

            frames.Add(new FrameInfo
            {
                Image = image,
                Delay = delay,
                ShouldDispose = dispose
            });
        }

        protected override void Dispose(bool disposing)
        {
            if (frames != null)
            {
                foreach (var frame in frames)
                {
                    if (frame.ShouldDispose)
                        frame.Image.Dispose();
                }
                frames = null;
            }
            base.Dispose(disposing);
        }

        private void CheckSequenceNonEmpty()
        {
            if (frames == null)
                throw new ObjectDisposedException("GdiImageSequence");
            if (frames.Count == 0)
                throw new InvalidOperationException("The image sequence is empty.");
        }

        public override int Width
        {
            get
            {
                CheckSequenceNonEmpty();
                return frames[0].Image.Width;
            }
        }

        public override int Height
        {
            get
            {
                CheckSequenceNonEmpty();
                return frames[0].Image.Height;
            }
        }

        public override int FrameCount
        {
            get
            {
                if (frames == null)
                    throw new ObjectDisposedException("GdiImageSequence");
                return frames.Count;
            }
        }

        public override int FrameIndex
        {
            get
            {
                CheckSequenceNonEmpty();
                return currentIndex;
            }
            set
            {
                CheckSequenceNonEmpty();
                if (value < 0 || value >= frames.Count)
                    throw new ArgumentOutOfRangeException("value");
                currentIndex = value;
            }
        }

        public override Image Frame
        {
            get
            {
                CheckSequenceNonEmpty();
                return frames[currentIndex].Image;
            }
        }

        public override TimeSpan FrameDelay
        {
            get
            {
                CheckSequenceNonEmpty();
                return new TimeSpan(10000 * frames[currentIndex].Delay);
            }
        }
    }
}
